import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetAddress;
import java.net.Socket;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.swing.JProgressBar;

public class XcherClient implements Runnable {
    /*
     * An object of this class represent the work an XCHER client thread is
     * running. Such a thread is created when a remote host requests a
     * connection by sending an X_INIT packet. XCHER works in a reverse connect
     * mode which basically means that an XCHERClient could be considered a
     * server in the classic definition of client-server relationship.
     * "the host requesting the connection is the client". Either way, this
     * XCHERClient connects to the ip+port received in the X_INIT packet. Then
     * listens for commands sent by the server running at the remote host.
     */
    private Socket commandSocket = null;
    private InetAddress address = null;
    private static ConcurrentHashMap<String, XcherData> fileListData;
    private BufferedReader input;
    private final Pattern regex = Pattern
	    .compile("[^\\s\"']+|\"[^\"]*\"|'[^']*'");
    private final ChatClient chatClient;
    // saves upload threads. So we can cancel them.
    private final LinkedList<Thread> threadsList;

    public XcherClient(String ip, String port, ChatClient clientThread) {
	threadsList = new LinkedList<Thread>();
	chatClient = clientThread;
	try {
	    address = InetAddress.getByName(ip.trim());
	    commandSocket = new Socket(address, Integer.parseInt(port));
	    input = new BufferedReader(new InputStreamReader(
		    commandSocket.getInputStream(), "UTF-8"));
	    // Since fileListData is static we only need to read shared.txt
	    // once.
	    if (fileListData == null) {
		initFileList();
	    }
	} catch (Exception e) {
	}
    }

    @Override
    public void run() {

	try {
	    ArrayList<String> matchList = new ArrayList<String>();

	    while (!commandSocket.isClosed()) {

		String msg = input.readLine();
		matchList.clear();

		Matcher regexMatcher = regex.matcher(msg);
		while (regexMatcher.find()) {
		    matchList.add(regexMatcher.group());
		}

		String nTmp = matchList.get(0);

		if (nTmp.equals("X_CHK")) {
		    matchList.set(1, matchList.get(1).replace("\"", ""));
		    actCheck(matchList);
		} else if (nTmp.equals("X_FILELIST")) {
		    actFileList();
		} else if (nTmp.equals("X_REQUEST")) {
		    matchList.set(1, matchList.get(1).replace("\"", ""));
		    actRequest(matchList);
		} else {
		    disconnect();
		}

	    }
	} catch (Exception e) {

	}
    }

    private void actCheck(ArrayList<String> matchList) throws IOException {
	/*
	 * XCHER server requested a filename lookup. These are sent if the
	 * server is searching for a specific file among all the clients. This
	 * method checks whether there's a file being shared on this host that
	 * matches the filename then returns the result to the server.
	 */
	XcherData fData = null;

	for (XcherData s : fileListData.values()) {
	    if (s.getFileName().equals(matchList.get(1))) {
		fData = s;
		break;
	    }
	}

	// Query a client for a specific file.
	// This will be useful for "Search All" feature of the chat client.
	if (fData != null) {
	    String filename = fData.getFileName();
	    if (fData.getFileName().contains(" ")) {
		filename = "\"" + fData.getFileName() + "\"";
	    }

	    commandSocket.getOutputStream().write(
		    ("X_CHK " + filename + " YES\r\n").getBytes("UTF-8"));
	} else {
	    // does not have the file.
	    String filename = matchList.get(1);
	    if (filename.contains(" ")) {
		filename = "\"" + filename + "\"";
	    }
	    commandSocket.getOutputStream().write(
		    ("X_CHK " + filename + " NO\r\n").getBytes("UTF-8"));
	}
    }

    private void actFileList() throws IOException {
	/*
	 * XCHER server requested this host filelist. These are sent if the
	 * client running the server wants to browse the shared files on this
	 * host. This method goes through its list of files and sends each in a
	 * seperete packet. Once the list has been processed it sends a final
	 * packet with EOF EOF to let the server know its done.
	 */

	// send each file info separately.
	for (XcherData value : fileListData.values()) {
	    String filename = value.getFileName();
	    if (filename.contains(" ")) {
		filename = "\"" + filename + "\"";
	    }
	    commandSocket.getOutputStream().write(
		    ("X_FILELIST " + filename + " " + value.getSize() + "\r\n")
			    .getBytes("UTF-8"));
	}
	// Send end of file.
	commandSocket.getOutputStream().write(
		("X_FILELIST EOF EOF\r\n").getBytes("UTF-8"));

    }

    private void actRequest(ArrayList<String> matchList) {
	/*
	 * XCHER server requested to download a file. This method translates
	 * filename+size into the local path for the specific file. Then gets
	 * the ip+port to which the fileSender thread is suppose to connect to
	 * begin sending the file data.
	 */

	XcherData fData = fileListData.get(matchList.get(1) + matchList.get(2));
	String remoteIP = commandSocket.getInetAddress().getHostAddress();
	int port = Integer.parseInt(matchList.get(3));

	FileSender t = new FileSender(new File(fData.getPath()), remoteIP,
		port, fData.getSize());
	Thread temp = new Thread(t);
	temp.start();
	// add to list of threads which is used for canceling running
	// uploads.
	threadsList.add(temp);
    }

    private void disconnect() throws IOException {
	/*
	 * This method is called to shutdown the XCHER session.
	 */

	// interrupt and shutdown all currently running uploads from this
	// user.
	for (Thread i : threadsList) {
	    i.interrupt();
	}
	commandSocket.close();
    }

    private void initFileList() {
	/*
	 * This method opens up "Shared.txt" in the workdir of this running
	 * ChatClient. It then parses each line as a filepath to a shared file.
	 * This method is only run once, when the first X_INIT for this
	 * chatclient is received.
	 * 
	 * Note: If the user running the chatClient does not have permission to
	 * read the file specified in the parsed path it will be ignored.
	 */

	fileListData = new ConcurrentHashMap<String, XcherData>();

	try {
	    FileInputStream fstream = new FileInputStream("Shared.txt");

	    BufferedReader br = new BufferedReader(new InputStreamReader(
		    fstream));
	    String strLine;
	    while ((strLine = br.readLine()) != null) {

		File tempFile = new File(strLine);
		String fileName = tempFile.getName();
		String path = tempFile.getAbsolutePath();

		// make sure the file can be read and that it hasn't been added
		// previously.
		if (tempFile.canRead()
			&& !fileListData.contains(fileName + tempFile.length())) {

		    fileListData.put((fileName + tempFile.length()),
			    new XcherData(path, fileName));
		}
	    }
	    // Close the input stream
	    br.close();
	} catch (Exception e) {
	    // Catch exception if any. If this occurred there probably wasn't a
	    // Shared.txt in workdir. We can let this pass without error
	    // handling because it means the filelist will be empty for this
	    // user. He will however be able to download files anyway.

	}
    }

    private class FileSender implements Runnable {
	/*
	 * This is the work carried out by the FileSender threads. All it does
	 * is read the requested file data then send it to the server who
	 * requested the download. Lastly it updates the upload progress bars on
	 * the GUI of this chatclient.
	 */
	private final File source;
	private final String ip;
	private final int port;
	private final int size;
	private final JProgressBar pbar;

	public FileSender(File file, String ip, int port, int size) {
	    source = file;
	    this.port = port;
	    this.ip = ip;
	    this.size = size;
	    pbar = chatClient.getGui().addNewUploadProgressBar(file.getName());
	}

	@Override
	public void run() {
	    Socket senderSocket = null;
	    FileInputStream fileInputStream = null;

	    try {
		senderSocket = new Socket(InetAddress.getByName(ip), port);
		java.io.OutputStream output = senderSocket.getOutputStream();
		fileInputStream = new FileInputStream(source);
		byte[] buffer = new byte[16 * 1024];

		float bytesReadTotal = 0;
		int bytesRead = 0;
		// read file to buffer and send until it reaches EOF or this
		// thread was interrupted(manually killed from gui).
		while (((bytesRead = fileInputStream.read(buffer)) > 0)
			&& !Thread.interrupted()) {
		    output.write(buffer, 0, bytesRead);
		    bytesReadTotal += bytesRead;
		    // update progress
		    pbar.setValue((int) ((bytesReadTotal / size) * 100));
		}

		chatClient.getGui().removeUploadProgressBar(pbar);
		senderSocket.close();
		fileInputStream.close();
	    } catch (Exception e) {
		try {
		    fileInputStream.close();
		    senderSocket.close();
		} catch (Exception e1) {
		}
		chatClient.getGui().removeUploadProgressBar(pbar);
	    }
	}
    }
}
